﻿open System
open System.Text
open System.Text.RegularExpressions
open System.IO

let autoTargetName src =
    let dir = Path.GetDirectoryName(src)
    let name = Path.GetFileNameWithoutExtension(src)
    let ext = Path.GetExtension(src)
    Path.Combine(dir, name + ".selected" + ext)

let parseArgs (args:string[]) =
    if args.Length < 1 then
        printfn "Default usage:"
        printfn "  CsvSelect <selection-file> <source-file> <target-file>"
        printfn "Derive target from source file demo.csv -> demo.selected.csv:"
        printfn "  CsvSelect <selection-file> <source-file>"
        printfn "Using selection.txt in the current folder as selection-file:"
        printfn "  CsvSelect <source-file>"
        None
    else
        let sFile = 
            match args.Length with
                | 1 -> "selection.txt"
                | _ -> args.[0]
        let src =
            match args.Length with
                | 1 -> args.[0]
                | _ -> args.[1]
        let trg = 
            match args.Length with
                | 3 -> args.[2]
                | _ -> autoTargetName src
        printfn "Selection: %s" sFile
        printfn "Source:     %s" src
        printfn "Target:     %s" trg
        Some(sFile, src, trg)

let trim (s:string) = s.Trim()

let trima = Array.map trim

let parseLine (l:string) = l.Split(',') |> trima

let parseColumn (l:string) = l.Split(':') |> trima

let readSelection file = File.ReadAllLines(file) |> Array.map parseLine

type DataType = IntType | FloatType | StringType | BoolType | UnknownType
type Relation = Less | LessEqual | Equal | NotEqual | GreaterEqual | Greater | Matches | UnknownRelation

let parseColumnDescr (d:string) =
    let parts = d.Split(':') |> trima
    if parts.Length <> 2 then
        failwith "The column description does not have two colon separated parts."
    let dataType = 
        match parts.[1].ToLowerInvariant() with
        | "int" -> IntType
        | "float" -> FloatType
        | "string" -> StringType
        | "bool" -> BoolType
        | _ -> UnknownType
    (parts.[0], dataType)

let parseRelOp (op:string) =
    match op.ToLowerInvariant() with
    | "<" -> Less
    | "<=" -> LessEqual
    | "=" -> Equal
    | "<>" -> NotEqual
    | ">=" -> GreaterEqual
    | ">" -> Greater
    | "match" -> Matches
    | _ -> UnknownRelation

let relationFunction dataType op (value2:string) =
    let nf = System.Globalization.CultureInfo.InvariantCulture.NumberFormat
    match dataType with
    | StringType -> match op with
                    | Less -> (fun value1 -> value1 < value2)
                    | LessEqual -> (fun value1 -> value1 <= value2)
                    | Equal -> (fun value1 -> value1 = value2)
                    | NotEqual -> (fun value1 -> value1 <> value2)
                    | GreaterEqual -> (fun value1 -> value1 >= value2)
                    | Greater -> (fun value1 -> value1 > value2)
                    | Matches -> let reg = new Regex(value2)
                                 (fun value1 -> reg.IsMatch(value1))
                    | _ -> failwith "This operator is not supported for strings."
    | IntType ->    let v2 = Int64.Parse(value2, nf)
                    match op with
                    | Less -> (fun v1 -> Int64.Parse(v1, nf) < v2)
                    | LessEqual -> (fun v1 -> Int64.Parse(v1, nf) <= v2)
                    | Equal -> (fun v1 -> Int64.Parse(v1, nf) = v2)
                    | NotEqual -> (fun v1 -> Int64.Parse(v1, nf) <> v2)
                    | GreaterEqual -> (fun v1 -> Int64.Parse(v1, nf) >= v2)
                    | Greater -> (fun v1 -> Int64.Parse(v1, nf) > v2)
                    | _ -> failwith "This operator is not supported for integer."
    | FloatType ->  let v2 = Double.Parse(value2, nf)
                    match op with
                    | Less -> (fun v1 -> Double.Parse(v1, nf) < v2)
                    | LessEqual -> (fun v1 -> Double.Parse(v1, nf) <= v2)
                    | Equal -> (fun v1 -> Double.Parse(v1, nf) = v2)
                    | NotEqual -> (fun v1 -> Double.Parse(v1, nf) <> v2)
                    | GreaterEqual -> (fun v1 -> Double.Parse(v1, nf) >= v2)
                    | Greater -> (fun v1 -> Double.Parse(v1, nf) > v2)
                    | _ -> failwith "This operator is not supported for float."
    | BoolType ->   let v2 = Boolean.Parse(value2)
                    match op with
                    | Equal -> (fun v1 -> Boolean.Parse(v1) = v2)
                    | NotEqual -> (fun v1 -> Boolean.Parse(v1) <> v2)
                    | _ -> failwith "This operator is not supported for boolean."
    | _ ->          failwith "This data type is not supported."

let parseRule (r:string[]) =
    if r.Length <> 3 then 
        failwith "The selection rule does not have three comma separated parts."
    let col, dataType = parseColumnDescr r.[0]
    let op = parseRelOp r.[1]
    let relFun = relationFunction dataType op r.[2]
    (col, relFun)

let rec parseRulesRec (rules:list<_>) (selection:string[][]) =
    if rules.Length = selection.Length then
        rules
    else
        parseRulesRec (parseRule selection.[rules.Length] :: rules) selection

let parseRules (selection:string[][]) = parseRulesRec [] selection

let indexRules (rules: list<string * _>) (head:string[]) =
    let indexer (col:string, relFun) =
        if Array.exists (fun c -> c = col) head then
            let i = Array.IndexOf(head, col)
            (Some(i), relFun)
        else
            (None, relFun)
    rules |> List.map indexer

let headAndRows (stream:Stream) =
    let r = new StreamReader(stream)
    let head = if r.EndOfStream then None else Some(r.ReadLine() |> parseLine)
    let rows = seq { while not r.EndOfStream do yield r.ReadLine() |> parseLine }
    (head, rows)

let runSelection selectionFile sourceFile targetFile =
    
    let rules = readSelection selectionFile |> parseRules

    use trgWr = new StreamWriter(targetFile, false, Encoding.Default, 16 * 1024)
    use srcStr = File.OpenRead(sourceFile)
    
    let write (l:string) = trgWr.WriteLine(l)
    
    let head, rows = headAndRows srcStr
    if head.IsNone then failwith "The data source has no head row."

    let indexedRules = indexRules rules head.Value

    write(String.Join(",", head.Value))
    let mutable cntAll = 0
    let mutable cntSel = 0
    for r in rows do
        cntAll <- cntAll + 1
        if indexedRules |> List.exists (fun (i, relFun) -> i.IsNone || relFun r.[i.Value]) then
            write(String.Join(",", r))
            cntSel <- cntSel + 1

    printfn "Finished. Selected %i data sets from %i." cntSel cntAll

    0

[<EntryPoint>]
let main args =
    match parseArgs args with
        | Some(sFile, src, trg) -> runSelection sFile src trg
        | None -> 1